package roborally.factory;

import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import roborally.supporting.PhysicalAmount;
import roborally.supporting.Unit;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;

/**
 * A factory that controls the making of new PhysicalAmounts. This factory will make sure that only one PhysicalAmount representing the same amount
 * (the same amount and the same unit) exists.
 * 
 * @invar
 * 		| hasProperPhysicalAmounts()
 * 
 * @author Molderez Tom, 2de bach Ingenieurswetenschappen Computerwetenschappen-Elektrotechniek and Reniers Jorn, 2de bach Ingenieurswetenschappen Werktuigkunde-Computerwetenschappen
 * @version 1.0
 *
 */
public class PhysicalAmountFactory {

	/**
	 * Returns this PhysicalAmountFactory object.
	 */
	@Basic @Immutable
	public static PhysicalAmountFactory get() {
		return physicalAmountFactory;
	}
	
	/**
	 * The PhysicalAmountFactory object used to create PhysicalAmounts.
	 */
	private static final PhysicalAmountFactory physicalAmountFactory = new PhysicalAmountFactory();

	/**
	 * Returns a new PhysicalAmount.
	 * 
	 * If the PhysicalAmount with the given parameters already exits, a copy of the reference will
	 * be returned. Otherwise a new PhysicalAmount will be created.
	 * When the arguments are not valid, an exception will be thrown.
	 * 
	 * @param amount The amount of this PhysicalAmount.
	 * @param unit The unit of this PhysicalAmount.
	 * @post This PhysicalAmount will be in the list of all existing amounts.
	 * 		| (new this).getPhysicalAmounts().contains(new PhysicalAmount(amount,unit))
	 * @return Returns a PhysicalAmount object with the given characteristics.
	 * 		| let temp be new PhysicalAmount(amount,unit)
	 * 		| if this.getPhysicalAmounts().containsKey(temp)
	 * 		|	then result == this.getPhysicalAmounts().get(temp)
	 * 		| else
	 * 		| 	then result == temp 
	 * @throws IllegalArgumentException
	 * 		If the unit is not a valid unit or if the amount is not valid.
	 * 		| !PhysicalAmount.canHaveAsUnit(unit) || !PhysicalAmount.canHaveAsAmount(amount)
	 */
	public PhysicalAmount create(double amount, Unit unit) throws IllegalArgumentException {
		PhysicalAmount temp = new PhysicalAmount(amount, unit);
		if(getPhysicalAmounts().containsKey(temp))
			return getPhysicalAmounts().get(temp);
		else {
			physicalAmounts.put(temp, temp);
			return temp;
		}
	}
	
	/**
	 * Deletes the physicalAmount so that it can be removed by the garbage collector.
	 * 
	 * @param physicalAmount The physicalAmount that must be removed.
	 */
	public void deletePhysicalAmount(PhysicalAmount physicalAmount) {
		physicalAmounts.remove(physicalAmount);
	}
	
	/**
	 * Returns all the physicalAmounts that are created.
	 */
	@Basic
	public Map<PhysicalAmount, PhysicalAmount> getPhysicalAmounts() {
		return Collections.unmodifiableMap(physicalAmounts);
	}
	
	/**
	 * Checks whether the PhysicalAmounts are valid.
	 * Therefore, all PhysicalAmounts must be valid and every PhysicalAmount may only exists once.
	 * 
	 * @return True if and only if every PhysicalAmount satisfies its classInvariants and every PhysicalAmount exists only once.
	 * 		| flag = true
	 * 		| for each physicalAmount in {PhysicalAmountFactory.getInstance().getPhysicalAmounts().keySet()}
	 * 		|	if physicalAmount == null || !PhysicalAmount.canHaveAsAmount(physicalAmount.getAmount()) || !PhysicalAmount.canHaveAsAmount((physicalAmount.getUnit())
	 * 		|		then flag = false
	 * 		|	for each physicalAmount2 in {PhysicalAmountFactory.getInstance().getPhysicalAmounts().keySet()}
	 * 		|		if physicalAmount2.equals(physicalAmount) && physicalAmount2 != physicalAmount
	 * 		|			then flag = false
	 * 		| result == flag
	 */
	public static boolean hasProperPhysicalAmounts(){
		for(PhysicalAmount physicalAmount: PhysicalAmountFactory.get().getPhysicalAmounts().keySet()){
			if(physicalAmount == null || !PhysicalAmount.isValidAmount(physicalAmount.getAmount()) || !PhysicalAmount.isValidUnit(physicalAmount.getUnit()))
				return false;
			for (PhysicalAmount physicalAmount2 : PhysicalAmountFactory.get().getPhysicalAmounts().keySet()){
				if (physicalAmount2.equals(physicalAmount) && physicalAmount2 != physicalAmount)
					return false;
			}
		}
		return true;
	}
	
	/**
	 * A map containing all the PhysicalAmount-objects that are created. A synchronized map is needed for supporting threads.
	 */
	private static Map<PhysicalAmount, PhysicalAmount> physicalAmounts = new ConcurrentHashMap<PhysicalAmount, PhysicalAmount>();//A map can search and return objects
	// in constant time. Using a set would require linear search to return the PhysicalAmount if it already exists.This makes everything slow down a lot.
	
	
	/**
	 * Returns a textual specification of this PhysicalAmountFactory.
	 * 
	 * @return A textual specification of this PhysicalAmountFactory.
	 * 		| result == this.getClass().getSimpleName() + ": [Legal status: " + PhysicalAmountFactory.hasProperPhysicalAmounts() + "], [Total: " + getPhysicalAmounts.size() + "], " +
	 *		|	"[PhysicalAmounts: " + getPhysicalAmounts().values().toString() + "]"
	 */
	@Override
	public String toString() {
		return this.getClass().getSimpleName() + ": [Legal status: " + PhysicalAmountFactory.hasProperPhysicalAmounts() + "], [Total: " + getPhysicalAmounts().size() + "], " +
				"[PhysicalAmounts: " + getPhysicalAmounts().values().toString() + "]";
	}
}
